iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Software Development

從Servlet到Spring MVC系列 第 24

Day23 Servlet - Project

  • 分享至 

  • xImage
  •  

前言

今日將與chatgpt協同合作一口氣將前面所用到的寫成一個專案,當中包含登入驗證,撰寫CRUD程式但我們不會串接到資料庫。

0、創建module

請參考Day05創建module

一、專案規劃

1. 這是一個簡易的員工後台管理系統

2. 前後端透過json數據進行溝通

3. 使用JWT進行認證

4. 省略資料庫串接

二、前端規劃

1. 登入頁index.html

  • 登入失敗顯示錯誤訊息
  • 接收登入成功則訪問/crud頁面

2. 員工crud頁面功能crud.html

  • 進入頁面載入所有員工
  • 新增員工
  • 編輯員工
  • 刪除員工
  • 登出功能

三、後端規劃

(1) 前後端規劃統一json溝通格式

  • success 表示請求是否成功
  • message 後端顯示訊息
  • data 後端回傳數據
{
    "success": true,
    "message": "XXXX",
    "data": [...]
}

(1) /login

  • 身分驗證
  • 給予認證token

(2) 資料存取api設計

  • GET /api/employees:獲取所有員工資料
  • POST /api/employees:新增員工
  • PUT /api/employees/{id}:更新員工資料
  • DELETE /api/employees/{id}:刪除員工

(3) Utils

  • JsonUtils 封裝JSON常用操作
  • JwtUtils 封裝JWT常用操作

(4) Filter

驗證訪問/api時的JWT驗證

四、實作

(1) 專案目錄

https://ithelp.ithome.com.tw/upload/images/20241008/20128084KCnF00YGM3.png

(2) pom

  <dependencies>
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
    <!-- JSON   -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.2</version>
    </dependency>
    <!-- JWT   -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.12.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.12.6</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
      <version>0.12.6</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>

(3) 前端頁面

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login Page</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f0f0;
            font-family: Arial, sans-serif;
        }
        .login-container {
            width: 300px;
            padding: 20px;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        .login-container h2 {
            text-align: center;
            margin-bottom: 20px;
        }
        .login-container input {
            width: calc(100% - 20px);
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .login-container button {
            width: 100%;
            padding: 10px;
            background-color: #007bff;
            color: #fff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .login-container button:hover {
            background-color: #0056b3;
        }
        .error-message {
            color: red;
            text-align: center;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>

<div class="login-container">
    <h2>Login</h2>
    <div class="error-message" id="error-message"></div>
    <div class="form-group">
        <label for="username">使用者帳號</label>
        <input type="text" class="form-control" id="username" name="username" required>
    </div>
    <div class="form-group">
        <label for="password">密碼</label>
        <input type="password" class="form-control" id="password" name="password" required>
    </div>
    <button onclick="login()">Login</button>
</div>

<script>
    function login() {
        const username = document.getElementById('username').value;
        const password = document.getElementById('password').value;
        const errorMessage = document.getElementById('error-message');

        // 清空錯誤訊息
        errorMessage.textContent = '';

        // 發送請求至後端 API
        fetch('/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username, password })
        })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert("login success")
                    // 跳轉到 crud.html 頁面
                    window.location.href = '/crud';
                    //window.open ='/crud';
                } else {
                    // 顯示錯誤訊息
                    errorMessage.textContent = data.message || 'Login failed!';
                }
            })
            .catch(error => {
                // 處理請求錯誤
                errorMessage.textContent = 'An error occurred. Please try again.';
                console.error('Error:', error);
            });
    }
</script>

</body>
</html>

crud.html

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CRUD 操作頁面</title>
    <!-- Bootstrap CSS -->
    <link
            href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
            rel="stylesheet"
    >
    <!-- 可選的自定義 CSS -->
    <style>
        body {
            background-color: #f8f9fa;
        }
        .crud-container {
            margin-top: 50px;
        }
        .crud-table {
            background-color: #ffffff;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        table {
            border-collapse: collapse;
            width: 100%;
        }

        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }

        thead {
            background-color: #f2f2f2;
        }

        tbody {
            display: block;
            height: 300px; /* 設定 tbody 的高度 */
            overflow-y: auto; /* 允許垂直滾動 */
            width: 100%;
        }

        tr {
            display: table;
            width: 100%;
            table-layout: fixed; /* 固定表格布局 */
        }
    </style>
</head>
<body>

<!-- CRUD 操作頁面內容 -->
<div class="container crud-container">
    <div class="row">
        <div class="col-md-12">
            <div class="crud-table">
                <h2 class="mb-4">員工管理</h2>
                <button class="btn btn-success mb-3" data-toggle="modal" data-target="#createModal">新增員工</button>
                <button id="logout" class="btn btn-danger mb-3" >登出</button>
                <table class="table table-bordered table-striped">
                    <thead class="thead-dark">
                    <tr>
                        <th>員工ID</th>
                        <th>員工名稱</th>
                        <th>操作</th>
                    </tr>
                    </thead>
                    <tbody id="employeesTableBody">
                    <!-- 員工數據將由 JavaScript 動態填充 -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<!-- 新增員工模態框 -->
<div class="modal fade" id="createModal" tabindex="-1" role="dialog" aria-labelledby="createModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <form id="createUserForm">
                <div class="modal-header">
                    <h5 class="modal-title" id="createModalLabel">新增員工</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="關閉">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">

                    <div class="form-group">
                        <label for="createUserName">員工名稱</label>
                        <input type="text" class="form-control" id="createUserName" name="userName" required>
                    </div>

                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="submit" class="btn btn-success">新增</button>
                </div>
            </form>
        </div>
    </div>
</div>

<!-- 編輯員工模態框 -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <form id="editUserForm">
                <div class="modal-header">
                    <h5 class="modal-title" id="editModalLabel">編輯員工</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="關閉">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">

                    <input type="hidden" id="editUserId" name="userId">

                    <div class="form-group">
                        <label for="editUserName">員工名稱</label>
                        <input type="text" class="form-control" id="editUserName" name="userName" required>
                    </div>

                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="submit" class="btn btn-primary">存檔</button>
                </div>
            </form>
        </div>
    </div>
</div>

<!-- Bootstrap JS 和依賴項 -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

<script>
    $(document).ready(function() {
        // 載入所有員工資料
        loadEmployees();

        $('#logout').on('click',logout);

        // 新增員工表單提交
        $('#createUserForm').on('submit', function(event) {
            event.preventDefault();
            const userName = $('#createUserName').val();

            $.ajax({
                type: 'POST',
                url: '/api/employees',
                contentType: 'application/json',
                data: JSON.stringify({ name: userName }),
                success: function(response) {
                    alert('新增員工成功!');
                    $('#createModal').modal('hide');
                    loadEmployees();
                },
                error: function(xhr, status, error) {
                    alert('新增員工失敗:' + xhr.responseText);
                }
            });
        });

        // 編輯員工表單提交
        $('#editUserForm').on('submit', function(event) {
            event.preventDefault();
            const userId = $('#editUserId').val();
            const userName = $('#editUserName').val();

            $.ajax({
                type: 'PUT',
                url: '/api/employees/' + userId,
                contentType: 'application/json',
                data: JSON.stringify({ name: userName }),
                success: function(response) {
                    alert('更新員工成功!');
                    $('#editModal').modal('hide');
                    loadEmployees();
                },
                error: function(xhr, status, error) {
                    alert('更新員工失敗:' + xhr.responseText);
                }
            });
        });

        // 編輯按鈕點擊事件
        $(document).on('click', '.edit-btn', function() {
            const userId = $(this).data('id');
            const userName = $(this).data('name');

            $('#editUserId').val(userId);
            $('#editUserName').val(userName);
            $('#editModal').modal('show');
        });

        // 刪除按鈕點擊事件
        $(document).on('click', '.delete-btn', function() {
            const userId = $(this).data('id');

            if (confirm('確定要刪除這個員工嗎?')) {
                $.ajax({
                    type: 'DELETE',
                    url: '/api/employees/' + userId,
                    success: function(response) {
                        alert('刪除員工成功!');
                        loadEmployees();
                    },
                    error: function(xhr, status, error) {
                        alert('刪除員工失敗:' + xhr.responseText);
                    }
                });
            }
        });

        // 函數:載入所有員工資料
        function loadEmployees() {
            $.ajax({
                type: 'GET',
                url: '/api/employees',
                dataType: 'json',
                success: function(result) {
                    console.log("GET /api/employees response:", result); // 增加日誌

                    if (result.success) {
                        const tbody = $('#employeesTableBody');
                        tbody.empty();

                        // 確保 result.data 是一個陣列
                        if (Array.isArray(result.data)) {
                            result.data.forEach(function(employee) {
                                const row = `
                            <tr>
                                <td>${employee.id}</td>
                                <td>${employee.name}</td>
                                <td>
                                    <button class="btn btn-primary btn-sm edit-btn" data-id="${employee.id}" data-name="${employee.name}">編輯</button>
                                    <button class="btn btn-danger btn-sm delete-btn" data-id="${employee.id}">刪除</button>
                                </td>
                            </tr>
                        `;
                                tbody.append(row);
                            });
                        } else {
                            console.error("Expected data to be an array, but got:", typeof result.data);
                            alert('載入員工資料失敗:資料格式錯誤');
                        }
                    } else {
                        alert('載入員工資料失敗:' + result.message);
                    }
                },
                error: function(xhr, status, error) {
                    console.error("AJAX GET /api/employees error:", error);
                    alert('載入員工資料失敗:' + xhr.responseText);
                }
            });
        }
        function logout() {
            // 清除存儲在 cookie 中的 token
            document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; // 清除 token cookie
            alert("logout success!!!");
            window.location.href = '/index.html'; // 導向登入頁
        }
    });
</script>
</body>
</html>

(4) 後端程式

bean - User

public class User {
  private String username;
  private String password;
  //省略getter setter constructor
}

bean - Employee

public class Employee {
  private int id;
  private String name;
  //省略getter setter constructor
}

bean - Result

public class Result<T> {
  private boolean success;
  private String message;
  private T data;
   //省略getter setter constructor
}

EmployeeDao

public class EmployeeDao {
    private static List<Employee> employees;
    static {
        employees = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            Employee e = new Employee(i,"emp"+i);
            employees.add(e);
        }
    }

    public List<Employee> getAllEmployees() {
        // 返回員工的複製,以防止外部修改
        synchronized (employees) {
            return new ArrayList<>(employees);
        }
    }

    public Employee createEmployee(Employee employee) {
        // 分配新的ID
        int newId = employees.stream().mapToInt(e -> e.getId()).max().getAsInt() +1;
        employee.setId(newId);
        employees.add(employee);
        return employee;
    }

    public boolean updateEmployee(int id, Employee updatedEmployee) {
        synchronized (employees) {
            for (Employee emp : employees) {
                if (emp.getId() == id) {
                    emp.setName(updatedEmployee.getName());
                    return true;
                }
            }
        }
        return false; // 未找到該員工
    }

    public boolean deleteEmployee(int id) {
        synchronized (employees) {
            return employees.removeIf(emp -> emp.getId() == id);
        }
    }

}

EmployeeService

public class EmployeeService {
    private EmployeeDao employeeDao;

    public EmployeeService() {
        this.employeeDao = new EmployeeDao();
    }

    public List<Employee> getAllEmployees() {
        return employeeDao.getAllEmployees();
    }

    public Employee createEmployee(Employee employee) {
        return employeeDao.createEmployee(employee);
    }


    public boolean updateEmployee(int id, Employee employee) {
        return employeeDao.updateEmployee(id, employee);
    }

    public boolean deleteEmployee(int id) {
        return employeeDao.deleteEmployee(id);
    }

}

LoginServlet

@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    private final ObjectMapper objectMapper = new ObjectMapper();

    // 假設的員工數據庫驗證(僅為示例,實際應用中請使用安全的驗證方式)
    private boolean authenticate(User user) {
        // 在這裡進行員工名和密碼的驗證
        // 這裡簡單示範,實際應用應該查詢數據庫或其他員工存儲
        return "admin".equals(user.getUsername()) && "1234".equals(user.getPassword());
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 設置回應的內容類型為 JSON 並指定字符編碼
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        // 解析請求體中的 JSON 數據
        User user = JsonUtils.readJson(request, User.class);
        System.out.println(user);
        // 準備回應的 JSON 數據
        Result result = new Result();

        if (authenticate(user)) {
            // 創建token
            String token = JwtUtils.createToken(user.getUsername());
            System.out.println(token);
            // 設置 Cookie
            Cookie jwtCookie = new Cookie("token", token);
            jwtCookie.setHttpOnly(true); // 防止 JavaScript 存取,減少 XSS 攻擊風險
            jwtCookie.setPath("/");
            jwtCookie.setMaxAge(24 * 60 * 60); // 1 天

            response.addCookie(jwtCookie);
            result.setSuccess(true);
            result.setMessage("Login successful!");
        } else {
            // 如果驗證失敗
            result.setSuccess(false);
            result.setMessage("Invalid username or password.");
        }

        JsonUtils.writeJson(response,result);

    }

}

CrudServlet

@WebServlet("/crud")
public class CrudServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("WEB-INF/crud.html").forward(req,resp);
    }
}

EmployeeServlet

@WebServlet(urlPatterns = {"/api/employees", "/api/employees/*"})
public class EmployeeServlet extends HttpServlet {
    private EmployeeService employeeService = new EmployeeService();

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Employee> employees = employeeService.getAllEmployees();
        Result<List<Employee>> result = new Result<>(true, "Employees fetched successfully", employees);

        JsonUtils.writeJson(response, result);
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Employee employee = JsonUtils.readJson(request, Employee.class);
        employeeService.createEmployee(employee);
        Result result = new Result(true, "Employee saved successfully");

        JsonUtils.writeJson(response, result);
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Employee employee = JsonUtils.readJson(req, Employee.class);
        String pathInfo = req.getPathInfo();
        String empId = pathInfo.split("/")[1];
        employee.setId(Integer.parseInt(empId));

        boolean isSuccess = employeeService.updateEmployee(employee.getId(), employee);
        String message = isSuccess?"Employee updated successfully":"Employee update failed";
        Result result = new Result(isSuccess, message);

        JsonUtils.writeJson(resp, result);
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String pathInfo = req.getPathInfo();
        String empId = pathInfo.split("/")[1];

        boolean isSuccess = employeeService.deleteEmployee(Integer.parseInt(empId));
        String message = isSuccess?"Employee deleted successfully":"Employee deleted failed";
        Result result = new Result(isSuccess, message);

        JsonUtils.writeJson(resp, result);

    }
}

JwtAuthenticationFilter

@WebFilter("/api/*")
public class JwtAuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        HttpServletRequest req = (HttpServletRequest)request;
        System.out.println(req.getRequestURI());
        System.out.println("filter do.......");
        // 從 Cookie 中獲取 JWT Token
        String token = getTokenFromCookies((HttpServletRequest) request);
        System.out.println("token: " + token);
        if (token != null && JwtUtils.validateToken(token)) {
            System.out.println("filter passed");
            chain.doFilter(request, response);
        }else {
            System.out.println("filter redirect to index.html");
            res.sendRedirect("/index.html");
        }
    }

    // 從 Cookie 中獲取 JWT Token
    private String getTokenFromCookies(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            return Arrays.stream(cookies)
                    .filter(cookie -> "token".equals(cookie.getName()))
                    .map(Cookie::getValue)
                    .findFirst()
                    .orElse(null); // 返回找到的 JWT Token,或 null
        }
        return null;
    }
}

JsonUtils

public class JsonUtils {
    private static ObjectMapper objectMapper = new ObjectMapper();

    public static <T> T readJson(HttpServletRequest rq ,Class<T> clazz) throws IOException {
        // 解析請求體中的 JSON 數據
        BufferedReader reader = rq.getReader();
        T t = objectMapper.readValue(reader.readLine(), clazz);
        return t;
    }

    public static void writeJson(HttpServletResponse response, Result result){
        response.setContentType("application/json;charset=UTF-8");
        try {
            String json = objectMapper.writeValueAsString(result);
            response.getWriter().write(json);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

JwtUtils

public class JwtUtils {
    private static final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    // Token 有效期設置
    private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 12; // 60 * 12 minutes

    public static String createToken(String username) {
        String token = Jwts.builder()
                .setSubject(username) // 使用者名稱
                .setIssuedAt(new Date()) // Token 發行時間
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(key) // 使用自定義 Secret Key 簽名
                .compact();

        return token;
    }

    public static boolean validateToken(String token) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(key) // 使用相同的 Secret Key 來驗證 Token
                    .build()
                    .parseSignedClaims(token)
                    .getBody();
            boolean isValid = claims.getExpiration().after(new Date());
            System.out.println(isValid);
            System.out.println(claims);
            return true;
        }catch(ExpiredJwtException e){
            System.out.println("token expired");
            e.printStackTrace();
            return false;
        }catch(SignatureException e){
            System.out.println("signature error");
            e.printStackTrace();
            return false;
        }
    }
}

四、Demo

(1) 首頁

https://ithelp.ithome.com.tw/upload/images/20241008/20128084Ke958LCm3c.png

(2) crud頁面

https://ithelp.ithome.com.tw/upload/images/20241008/20128084txFoE7Kxoi.png

Reference


上一篇
Day22 Servlet - MVC
下一篇
Day24 Spring MVC - Review Spring Framework (1)
系列文
從Servlet到Spring MVC36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言